package nl.utwente.ewi.hmi.tuioserver;

import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import nl.utwente.ewi.hmi.multitouch.Touch;
import nl.utwente.ewi.hmi.multitouch.TouchDevice;
import nl.utwente.ewi.hmi.multitouch.TouchDeviceInfo;
import nl.utwente.ewi.hmi.multitouch.TouchDeviceListener;
import nl.utwente.ewi.hmi.multitouch.TouchEnvironment;
import nl.utwente.ewi.hmi.multitouch.TouchDevice.TouchFrameProcessor;
import nl.utwente.ewi.hmi.multitouch.drivers.calibrate.CalibratedTouchStreamProvider;
import nl.utwente.ewi.hmi.multitouch.drivers.calibrate.FixedTransformCalibrator;
import nl.utwente.ewi.hmi.multitouch.drivers.diamondtouch.DiamondTouchStreamProvider;
import nl.utwente.ewi.hmi.multitouch.drivers.tuio.TuioTouchStreamProvider;
import nl.utwente.ewi.hmi.multitouch.event.TouchEvent;
import nl.utwente.ewi.hmi.multitouch.event.TouchJoinEvent;
import nl.utwente.ewi.hmi.multitouch.event.TouchListener;
import nl.utwente.ewi.hmi.multitouch.event.TouchSplitEvent;
import nl.utwente.ewi.hmi.multitouch.io.TouchStreamProvider;

import com.illposed.osc.OSCBundle;
import com.illposed.osc.OSCMessage;
import com.illposed.osc.OSCPortOut;

/**
 * A simple TuioServer for input received from the mu3 framework.
 * 
 * @author Michiel Hakvoort
 * @version 0.1
 *
 */
public class TuioServer implements Runnable, TouchDeviceListener, TouchListener {

	private Map<Touch, Integer> touches = null;
	
	private int frame = 0;
	
	private Set<Touch> updatedTouches = null;
	
	private long shortUpdateTime = 0;
	private long longUpdateTime = 0;

	private enum TouchDeviceType {
		TUIO, DIAMONDTOUCH
	}

	private OSCPortOut oscOut = null;

	private Thread self = null;

	private TouchFrameProcessor processor = null;
	private TouchDevice device = null;

	private int sessionId = 1;

	/**
	 * Create a new TuioServer for the given device, and bind to the given address and port.
	 * 
	 * @param device The device to propagate as a TUIO device
	 * @param host The host address to bind to
	 * @param port The port to bind to
	 * 
	 * @throws IOException
	 */
	public TuioServer(TouchDevice device, InetAddress host, int port) throws IOException {
		oscOut = new OSCPortOut(host, port);
		self = new Thread(this);

		device.addTouchDeviceListener(this);

		this.device = device;
		this.device.setThreaded(false);

		updatedTouches = new CopyOnWriteArraySet<Touch>();

		this.processor = device.getTouchFrameProcessor();
		this.touches = new HashMap<Touch, Integer>();

	}

	public void start() throws IOException {
		this.device.start();
		self.start();
		
	}

	public synchronized boolean isRunning() {
		return self != null;
	}

	public void run() {
		while(isRunning()) {


			processor.process();
			long now = System.currentTimeMillis();
			
			
			boolean issueShortUpdate = (now - shortUpdateTime) > 5;
			boolean issueLongUpdate = (now - longUpdateTime) > 1000;

			if(issueShortUpdate) {
				shortUpdateTime = now;
			}
			
			if(issueLongUpdate) {
				longUpdateTime = now;
			}

			if(!issueShortUpdate && !issueLongUpdate) {
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
				}

				continue;
			}

			// Remote message
			OSCMessage remoteMessage = new OSCMessage("/tuio/2Dcur");
			remoteMessage.addArgument("source");
			remoteMessage.addArgument("mu3proxy");	

			// Ping
			OSCMessage aliveMessage = new OSCMessage("/tuio/2Dcur");
			aliveMessage.addArgument("alive");

			for(Integer sessionId : touches.values()) {
				aliveMessage.addArgument(sessionId);
			}

			// Issue updates for modified Touches
			if(issueShortUpdate) {
				OSCMessage frameMessage = new OSCMessage("/tuio/2Dcur");
				frameMessage.addArgument("fseq");
				frameMessage.addArgument(frame);

				frame++;


				List<OSCMessage> updatedMessages = new LinkedList<OSCMessage>();

				boolean issueDelete = false;

				for(Touch touch : updatedTouches) {
					updatedTouches.remove(touch);

					if(!touches.containsKey(touch)) {
						issueDelete = true;
						continue;
					}

					OSCMessage setMessage = new OSCMessage("/tuio/2Dcur");

					setMessage.addArgument("set");
					setMessage.addArgument(new Integer(touches.get(touch)));
					setMessage.addArgument(new Float(touch.getOrigin().getX()));
					setMessage.addArgument(new Float(touch.getOrigin().getY()));
					setMessage.addArgument(new Float(touch.getVelocity().getX()));
					setMessage.addArgument(new Float(touch.getVelocity().getY()));

					setMessage.addArgument(new Float(0));

					updatedMessages.add(setMessage);
				}

				Iterator<OSCMessage> updatedMessageIterator = updatedMessages.iterator();
	
				while(updatedMessageIterator.hasNext()) {
					OSCBundle bundle = new OSCBundle();
						
					bundle.addPacket(remoteMessage);
					bundle.addPacket(aliveMessage);
		
					int messageCount = 0;
		
					while(updatedMessageIterator.hasNext() && messageCount < 5) {
						bundle.addPacket(updatedMessageIterator.next());
						messageCount++;
					}
						
					if(!updatedMessageIterator.hasNext()) {
						bundle.addPacket(frameMessage);
					}
	
					try {
						oscOut.send(bundle);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
				
				if(updatedMessages.isEmpty() && issueDelete) {
					OSCBundle bundle = new OSCBundle();
					bundle.addPacket(remoteMessage);
					bundle.addPacket(aliveMessage);
					bundle.addPacket(frameMessage);
	
					try {
						oscOut.send(bundle);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
			

			// Refresh the entire cursor state
			if(issueLongUpdate) {
				List<OSCMessage> messages = new LinkedList<OSCMessage>();

				// Frame message
				OSCMessage frameMessage = new OSCMessage("/tuio/2Dcur");
				frameMessage.addArgument("fseq");
				frameMessage.addArgument(-1);

				for(Map.Entry<Touch, Integer> entry : this.touches.entrySet()) {
					Touch touch = entry.getKey();
						
					OSCMessage setMessage = new OSCMessage("/tuio/2Dcur");
					setMessage.addArgument("set");
					setMessage.addArgument(new Integer(entry.getValue()));
					setMessage.addArgument(new Float(touch.getOrigin().getX()));
					setMessage.addArgument(new Float(touch.getOrigin().getY()));
					setMessage.addArgument(new Float(touch.getVelocity().getX()));
					setMessage.addArgument(new Float(touch.getVelocity().getY()));
						
					// Todo add acceleration
					setMessage.addArgument(new Float(0));
					messages.add(setMessage);
				}
				
	
				Iterator<OSCMessage> messageIterator = messages.iterator();
					
				// If there are messages to send
				while(messageIterator.hasNext()) {
					OSCBundle bundle = new OSCBundle();
						
					bundle.addPacket(remoteMessage);
					bundle.addPacket(aliveMessage);
		
					int messageCount = 0;
		
					while(messageIterator.hasNext() && messageCount < 5) {
						bundle.addPacket(messageIterator.next());
						messageCount++;
					}
						
					if(!messageIterator.hasNext()) {
						bundle.addPacket(frameMessage);
					}
						
					try {
						oscOut.send(bundle);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
					
				// If there are no messages to send
				if(messages.isEmpty()) {
					OSCBundle bundle = new OSCBundle();
					bundle.addPacket(remoteMessage);
					bundle.addPacket(aliveMessage);
					bundle.addPacket(frameMessage);

					try {
						oscOut.send(bundle);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}
	
	/**
	 * Start the TuioServer
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		InetAddress host = null;
		int port = 3333;
		TouchDeviceType type = TouchDeviceType.TUIO;
		
		
		/*
		 * Accept 
		 *    --hostname or -h for hostname
		 *    --port or -p for port
		 *    --device or -d for device selection
		 *
		 */
		for (int i=0;i<args.length;i++) {
			if (args[i].equalsIgnoreCase("--hostname") || args[i].equalsIgnoreCase("-h")) {
				i++;

				if(i < args.length) {
					try {
						host = InetAddress.getByName(args[i]);
					} catch (UnknownHostException e) {
						System.out.println("Could not identify host \"" + args[i] + "\"");
						System.exit(1);
					}
				}
			} else if(args[i].equalsIgnoreCase("--port") || args[i].equalsIgnoreCase("-p")) {
				i++;

				if(i < args.length) {
					try {
						port = Integer.parseInt(args[i]);
					} catch(NumberFormatException e) {
						System.out.println("Invalid port \"" + args[i] + "\"");
						System.exit(1);
					}
				}
			} else if(args[i].equalsIgnoreCase("--device") || args[i].equalsIgnoreCase("-d")) {
				i++;

				if(i < args.length) {
					if(args[i].equalsIgnoreCase("tuio")) {
						type = TouchDeviceType.TUIO;
					} else if(args[i].equalsIgnoreCase("diamondtouch")) {
						type = TouchDeviceType.DIAMONDTOUCH;
					} else {
						System.out.println("Invalid touch device type \"" + args[i] + "\"");
						System.exit(1);
					}
				}				
			}
		}
		
		TouchStreamProvider provider = null;

		// Select either TUIO or DiamondTouch
		switch(type) {
			case TUIO :
				provider = new TuioTouchStreamProvider();
				break;
			case DIAMONDTOUCH :
				provider = DiamondTouchStreamProvider.getDiamondTouchStreamProvider();
				break;
		}
		
		// Connect to localhost by default
		if(host == null) {
			try {
				host = InetAddress.getLocalHost();
			} catch (UnknownHostException e) {
				System.out.println("Could not identify localhost");
				System.exit(1);
			}
		}

		// Apply TUIO calibration (project all actions to the [0,1]x[0,1] square)
		provider = new CalibratedTouchStreamProvider(provider, new FixedTransformCalibrator(new Rectangle2D.Float(0, 0, 1, 1)));
		
		TouchEnvironment touchEnvironment = TouchEnvironment.getTouchEnvironment();
		touchEnvironment.registerTouchStreamProvider(provider);
		List<TouchDeviceInfo> devices = touchEnvironment.getTouchDeviceInfoList();

		if(devices.isEmpty()) {
			System.out.println("No touchdevices registered");
			System.exit(1);
		}

		TouchDevice device = touchEnvironment.getTouchDevice(devices.iterator().next());

		TuioServer server = null;

		// Open the socket by creating the server
		try {
			server = new TuioServer(device, host, port);
		} catch(IOException e) {
			System.out.println("Could not start server at " + host.getHostAddress() + ":" + port);
			System.exit(1);
		}

		// Start broadcasting TUIO messages from the wrapped TouchDevice
		try {
			server.start();
		} catch (IOException e) {
			System.out.println("Could not initialize touch device");
			System.exit(1);
		}

		System.out.println("Broadcasting to " + host.getHostAddress() + ":" + port);

	}

	@Override
	public void touchDeviceStarted(TouchDevice touchDevice) {
	}

	@Override
	public void touchDeviceStopped(TouchDevice touchDevice) {
	}

	@Override
	public void touchJoined(TouchJoinEvent event) {
	}

	@Override
	public void touchShapeChanged(TouchEvent event) {
	}
	
	@Override
	public void touchSplit(TouchSplitEvent event) {
	}

	@Override
	public void touchRegistered(TouchDevice touchDevice, Touch touch) {
		touch.addTouchListener(this);
		touches.put(touch, sessionId);
		updatedTouches.add(touch);
		sessionId++;
	}

	@Override
	public void touchMoved(TouchEvent event) {
		updatedTouches.add(event.getSource());
	}
	
	@Override
	public void touchReleased(TouchEvent event) {
		this.touches.remove(event.getSource());
		updatedTouches.add(event.getSource());
	}
}
